import * as Sentry from '@sentry/react';
import { execPythonMessage, evalPythonMessage } from '../components/general/GeppettoJupyterUtils';

// Extend window interface for metadata
declare global {
  interface Window {
    metadata?: any;
    IPython?: any;
    GEPPETTO?: any;
  }
}

const Utils = {
  getAvailableKey(model: Record<string, any> | undefined, prefix: string): string {
    if (model === undefined) {
      return prefix;
    }
    // Get New Available ID
    let id = prefix;
    if (model[`${id}0`] === undefined) {
      return `${id}0`;
    }
    let i = 0;
    id = prefix + i++;
    while (model[id] !== undefined) {
      id = prefix + i++;
    }
    return id;
  },

  captureSentryException(e: Error): void {
    if (process.env.NODE_ENV === 'production') {
      Sentry.captureException(e);
    }
    console.error(e);
  },

  /**
   * Retrieves the metadata object for the passed `key`.
   *
   * A specific attributes can be selected by passing `field`.
   *
   * The algorithm will iteratively test the key for each nesting in metadata.
   *
   * Example key: `netParams.popParams.E.numCells`.
   * Since 'E' is not explicitly modeled in `netpyne.metadata`
   * this algorithm implements additional logic to skip certain levels.
   *
   * @param key - key of metadata path
   * @param field - specific object attribute
   * @returns metadata object or specific attribute
   */
  getMetadataField(key: string | undefined, field: string | null = null): any {
    if (key === undefined) {
      return null;
    }

    let currentObject: any;
    let nextObject = window.metadata || {};
    let skipped = false;

    // eslint-disable-next-line no-restricted-syntax
    for (const item of key.split('.')) {
      if (currentObject != null && currentObject?.container === true && !(item in nextObject)) {
        if (skipped) {
          return null;
        }

        // skip the list element, e.g. "E"!
        skipped = true;
      } else {
        skipped = false;

        if (item in nextObject) {
          currentObject = nextObject[item];
          if ('children' in currentObject) {
            nextObject = currentObject.children;
          }
        } else {
          currentObject = null;
        }
      }
    }

    if (currentObject) {
      return field ? currentObject[field] : currentObject;
    }

    return null;
  },

  getHTMLType(key: string): string {
    const type = this.getMetadataField(key, 'type');

    switch (type) {
      case 'int':
        return 'number';
      default:
        return 'text';
    }
  },

  isObject(item: any): boolean {
    return item && typeof item === 'object' && !Array.isArray(item);
  },

  mergeDeep<T extends Record<string, any>>(target: T, source: T): T {
    const output: Record<string, any> = { ...target };
    if (this.isObject(target) && this.isObject(source)) {
      Object.keys(source).forEach((key) => {
        if (this.isObject(source[key])) {
          if (!(key in target)) {
            Object.assign(output, { [key]: source[key] });
          } else {
            output[key] = this.mergeDeep(target[key], source[key]);
          }
        } else {
          Object.assign(output, { [key]: source[key] });
        }
      });
    }
    return output as T;
  },

  getFieldsFromMetadataTree(tree: any, callback: (id: string, depth: number) => any): any[] {
    function iterate(object: any, path: (string | number)[]): void {
      if (Array.isArray(object)) {
        object.forEach((a, i) => {
          iterate(a, path.concat(i));
        });
        return;
      }
      if (object !== null && typeof object === 'object') {
        Object.keys(object).forEach((k) => {
          // Don't add the leaf to path
          iterate(object[k], typeof object[k] === 'object' ? path.concat(k) : path);
        });
        return;
      }

      // Push to array of field id. Remove children and create id string
      modelFieldsIds.push(
        path.filter((p) => p !== 'children').join('.')
      );
    }

    // Iterate the array extracting the fields Ids
    const modelFieldsIds: string[] = [];
    iterate(tree, []);

    // Generate model fields based on ids
    const modelFields: any[] = [];
    modelFieldsIds
      .filter((v, i, a) => a.indexOf(v) === i)
      .map((id) => modelFields.push(callback(id, 0)));
    return modelFields;
  },

  asEscapedString(value: string): string {
    return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  },

  renameKey(path: string, oldValue: string, newValue: string, callback: (response: any, newValue: string) => void): void {
    const escapedOldValue = oldValue.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
    const escapedNewValue = newValue.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
    this.execPythonMessage(`netpyne_geppetto.rename("${path}","${escapedOldValue}","${escapedNewValue}")`)
      .then((response: any) => {
        callback(response, newValue);
      });
  },

  nameValidation(name: string): string {
    // Remove spaces
    if ((/\s/.test(name))) {
      name = name.replace(/\s+/g, '').replace(/^\d+/g, '');
    } else if ((/^[0-9]/.test(name))) {
      // Remove number at the beginning
      name = name.replace(/\s+/g, '').replace(/^\d+/g, '');
    }
    return name;
  },

  // FIXME: Hack to remove escaped chars (\\ -> \ and \' -> ') manually
  convertToJSON(data: any): any {
    if (typeof data === 'string' || data instanceof String) {
      return JSON.parse(data.replace(/\\\\/g, '\\').replace(/\\'/g, '\''));
    }
    return data;
  },

  getPlainStackTrace(stackTrace: string): string {
    return stackTrace.replace(/\u001b\[.*?m/g, '');
  },

  getErrorResponse(data: any): { message?: string; details: any; additionalInfo?: any } | null {
    const parsedData = this.convertToJSON(data);
    if (parsedData && parsedData.type && parsedData.type === 'ERROR') {
      const error: { message?: string; details: any; additionalInfo?: any } = { details: parsedData.details };
      if (Object.prototype.hasOwnProperty.call(parsedData, 'message')) {
        error.message = parsedData.message;
      } else if (Object.prototype.hasOwnProperty.call(parsedData, 'websocket')) {
        error.message = parsedData.websocket;
      }
      if (Object.prototype.hasOwnProperty.call(parsedData, 'additional_info')) {
        error.additionalInfo = parsedData.additional_info;
      }
      return error;
    }
    return null;
  },

  handleUpdate(
    updateCondition: boolean,
    newValue: string,
    originalValue: string,
    context: any,
    componentName: string
  ): boolean {
    if (updateCondition && newValue !== originalValue) {
      /*
       * if the new value has been changed by the function Utils.nameValidation means that the name convention
       * has not been respected, so we need to open the dialog and inform the user.
       */
      context.setState({
        currentName: newValue,
        errorMessage: 'Error',
        errorDetails: `Leading digits or whitespaces are not allowed in ${componentName} names.`,
      });
      return true;
    }
    if (updateCondition && newValue === originalValue) {
      context.setState({ currentName: newValue });
      return true;
    }
    if (!updateCondition && newValue === originalValue) {
      context.setState({
        currentName: newValue,
        errorMessage: 'Error',
        errorDetails: `Name collision detected, the name ${newValue} is already used in this model, please pick another name.`,
      });
      return false;
    }
    if (!updateCondition && newValue !== originalValue) {
      context.setState({
        currentName: newValue,
        errorMessage: 'Error',
        errorDetails: `Leading digits or whitespaces are not allowed in ${componentName} names.`,
      });
      return false;
    }
    return false;
  },

  formatDate(timestamp: number | string | Date): string {
    const date = new Date(timestamp);
    return date?.toLocaleString() || '';
  },

  execPythonMessage,
  evalPythonMessage,

  flatten(obj: any, path = ''): Record<string, any> {
    if (!(obj instanceof Object)) return { [path.replace(/\.$/g, '')]: obj };

    return Object.keys(obj).reduce(
      (output, key) => (
        obj instanceof Array
          ? { ...output, ...Utils.flatten(obj[key], `${path.slice(0, -1)}[${key}].`) }
          : { ...output, ...Utils.flatten(obj[key], `${path + key}.`) }
      ),
      {}
    );
  },
};

export default Utils;

